Sending and Receiving Data
The
data handler (in this case, self) must implement the
receiveData:fromPeer:inSession:context: method. The data sent to this
method uses an NSData
object; there are no hooks or handles for partial data receipt and
processing. As the data arrives as a single chunk, keep your data
bursts short (under 1,000 bytes) and to the point for highly
interactive applications.
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
inSession: (GKSession *)session context:(void *)context
{
// handle data here
}
Send
data via the session object. You can send in reliable mode or
unreliable mode. Reliable mode uses error checking and retrying until
the data is properly sent. All items are guaranteed to arrive in the
order they are sent, using TCP transmission. With unreliable mode, data
is sent once using UDP transmission, with no retry, Data may arrive out
of order. Use reliable mode (GKSendDataReliable) when you must guarantee correct delivery and unreliable mode for short bursts of data that must arrive nearly instantaneously.
- (void) sendDataToPeers: (NSData *) data
{
// Send the data, checking for success or failure
NSError *error;
BOOL didSend = [self.session sendDataToAllPeers:data
withDataMode:GKSendDataReliable error:&error];
if (!didSend)
NSLog(@"Error sending data to peers: %@",
[error localizedDescription]);
}
As
a rule, the one error you’ll encounter here results from queuing too
much data in reliable mode. This produces a “buffer full” error.
State Changes
The
following session delegate callback lets you know when a peer’s state
has changed. The two states you want to look for are connected, that
is, when the connection finally happens after the peer picker has been
dismissed, and disconnected, when the other user quits the application,
manually disconnects, or moves out of range.
- (void)session:(GKSession *)session peer:(NSString *)peerID
didChangeState:(GKPeerConnectionState)state
{
/* STATES:
GKPeerStateAvailable, = 0,
GKPeerStateUnavailable, = 1,
GKPeerStateConnected, = 2,
GKPeerStateDisconnected, = 3,
GKPeerStateConnecting = 4 */
if (state == GKPeerStateConnected)
{
// handle connected state
}
if (state == GKPeerStateDisconnected)
{
// handle disconnection
}
}
To force a session to disconnect, use the disconnectFromAllPeers method.
- (void) disconnect
{
// Disconnect and then reset the session property
[self.session disconnectFromAllPeers];
self.session = nil;
}
Creating a GameKit Helper
Recipe 12-1
bundles the entire peer process into a simplified helper class. This
class hides most of the GameKit details connection and data transfer
details, while providing a demonstration of how to use these features.
More importantly, it breaks down how you might look at the GameKit
process, with its two key details: connection and data.
Connecting
Any
GameKit client you write must respond appropriately to the current
connection state. You need to be able to establish that connection and
respond when it goes live or when it drops. This class provides both connect and disconnect
requests. For the most part, monitoring connections involves toggling a
state Boolean (isConnected) and updating any buttons that control a
connect/disconnect toggle.
To simplify these updates, the class allows you to assign a view controller (via the viewController
property) and automatically updates the right-hand navigation item
button. The button starts off as Connect, and when tapped disappears
until the user cancels or a connection is fully established. After
connecting, the button updates to Disconnect and provides a callback to
the helper’s disconnect method.
Handling Data
By providing the connection state details for you, you can use this GameKitHelper
class to create simple GameKit-enabled applications. The data handling,
however, remains in your hands. Consider the following snippet. It
shows the entire implementation for a chat application view controller,
demonstrating the data transfer methods for this app.
@implementation TestBedViewController
- (void)textViewDidChange:(UITextView *)textView
{
// Perform updates only when connected
if (![GameKitHelper sharedInstance].isConnected) return;
NSString *text = sendView.text;
// Check for empty text. If so, send special clear request
if (!text || (text.length == 0)) text = @"xyzzyclear";
NSData *textData = [text dataUsingEncoding:NSUTF8StringEncoding];
[GameKitHelper sendData:textData];
}
-(void) receivedData: (NSData *) data
{
NSString *text = [[[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding] autorelease];
// Check for clear request when updating text
receiveView.text = [text isEqualToString:@"xyzzyclear"] ?
@"" : text;
}
- (void) clear
{
// Handle a clear request
sendView.text = @"";
}
- (void) viewDidLoad
{
self.navigationItem.leftBarButtonItem = BARBUTTON(@"Clear",
@selector(clear));
// Initialize the helper
[GameKitHelper sharedInstance].sessionID = @"Typing Together";
[GameKitHelper sharedInstance].dataDelegate = self;
[GameKitHelper assignViewController:self];
// Present the keyboard
[sendView becomeFirstResponder];
}
@end
As
you can see, this application monitors a “send” text view, and when it
changes (as the user types), sends the contents of that view through
GameKit to a peer. At the same time, it waits for data, and when it
receives it, updates the received text view to show what the peered
user has typed. A Clear button erases the “send” view text.
This application demonstrates the second half of the GameKit problem, handling data. Recipe 12-1’s
helper class creates a data delegate protocol, which is subscribed to
by this text chat view controller. Data is passed along through the
custom receivedData: delegate method, allowing the received text view to update with text typed on the peer device.
Similarly, the text view delegate method textViewDidChange: passes on responsibility for transmitting the actual text to the GameKitHelper class, calling the sendData: method to convey the data to connected peers.
Note
Recipe 12-1
does not address the issue of out-of-order packet receipt. See Apple’s
GKTank sample code for an example of network packet handling. Apple’s
code looks for the last packet time and the packet ID to ensure that
packets are handled in the proper sequence.
The Helper Class
Recipe 1 contains the implementation for the GameKitHelper
class. The associated sample code for this recipe shows the class in
action, creating the text chat application discussed previously. This
class was designed for reuse and can easily be decoupled from the text
chat and repurposed, as you see in the next recipe.
Recipe 1. GameKitHelper Class
@implementation GameKitHelper
@synthesize dataDelegate;
@synthesize viewController;
@synthesize sessionID;
@synthesize session;
@synthesize isConnected;
// Macro helps check and then send selectors for data
// delegate callbacks
#define DO_DATA_CALLBACK(X, Y) if (self.dataDelegate && \
[self.dataDelegate respondsToSelector:@selector(X)]) \
[self.dataDelegate performSelector:@selector(X) withObject:Y];
#pragma mark Shared Instance
static GameKitHelper *sharedInstance = nil;
+ (GameKitHelper *) sharedInstance
{
if(!sharedInstance) sharedInstance = [[self alloc] init];
return sharedInstance;
}
#pragma mark Data Sharing
// Send data to all connected peers
- (void) sendDataToPeers: (NSData *) data
{
NSError *error;
BOOL didSend = [self.session sendDataToAllPeers: data
withDataMode:GKSendDataReliable error:&error];
if (!didSend)
DO_DATA_CALLBACK(sentData:,
(didSend ? nil : [error localizedDescription]));
}
// Redirect data receipt to the data delegate
- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer
inSession: (GKSession *)session context:(void *)context
{
DO_DATA_CALLBACK(receivedData:, data);
}
#pragma mark Connections
// Start a new connection by presenting a peer picker
- (void) startConnection
{
if (!self.isConnected)
{
GKPeerPickerController *picker = [[GKPeerPickerController
alloc] init];
picker.delegate = self;
picker.connectionTypesMask = GKPeerPickerConnectionTypeNearby;
[picker show];
if (self.viewController)
self.viewController.navigationItem.rightBarButtonItem =
nil;
}
}
// Dismiss the peer picker on cancel
- (void) peerPickerControllerDidCancel:
(GKPeerPickerController *)picker
{
[picker release];
if (self.viewController)
self.viewController.navigationItem.rightBarButtonItem =
BARBUTTON(@"Connect", @selector(startConnection));
}
// Upon a successful connection, set up the data handler
- (void)peerPickerController:(GKPeerPickerController *)picker
didConnectPeer:(NSString *)peerID
toSession: (GKSession *) session
{
[picker dismiss];
[picker release];
isConnected = YES;
[self.session setDataReceiveHandler:self withContext:nil];
DO_DATA_CALLBACK(connectionEstablished, nil);
}
// Provide the session information including id and mode
- (GKSession *)peerPickerController:(GKPeerPickerController *)picker
sessionForConnectionType:(GKPeerPickerConnectionType)type
{
if (!self.session) {
self.session = [[GKSession alloc] initWithSessionID:
(self.sessionID ? self.sessionID : @"Sample Session")
displayName:nil sessionMode:GKSessionModePeer];
self.session.delegate = self;
}
return self.session;
}
#pragma mark Session Handling
// Disconnect the current session
- (void) disconnect
{
[self.session disconnectFromAllPeers];
self.session = nil;
}
// Detect when the other peer has changed its state
- (void)session:(GKSession *)session peer:(NSString *)peerID
didChangeState:(GKPeerConnectionState)state
{
if (state == GKPeerStateConnected)
{
if (self.viewController)
self.viewController.navigationItem.rightBarButtonItem =
BARBUTTON(@"Disconnect", @selector(disconnect));
}
if (state == GKPeerStateDisconnected)
{
self.isConnected = NO;
showAlert(@"Lost connection with peer. You are no longer \
connected to another device.");
[self disconnect];
if (self.viewController)
self.viewController.navigationItem.rightBarButtonItem =
BARBUTTON(@"Connect", @selector(startConnection));
}
}
// Utility method for setting up the view controller
- (void) assignViewController: (UIViewController *) aViewController
{
self.viewController = aViewController;
self.viewController.navigationItem.rightBarButtonItem =
BARBUTTON(@"Connect", @selector(startConnection));
}
#pragma mark Class utility methods
// These class methods redirect to instance methods.
// They're here for convenience only
+ (void) connect
{
[[self sharedInstance] startConnection];
}
+ (void) disconnect
{
[[self sharedInstance] disconnect];
}
+ (void) sendData: (NSData *) data
{
[[self sharedInstance] sendDataToPeers:data];
}
+ (void) assignViewController: (UIViewController *) aViewController
{
[[self sharedInstance] assignViewController:aViewController];
}
@end